Crate async_injector[][src]

Expand description

Documentation Crates Actions Status

Asynchronous dependency injection for Rust.

This crate provides a dependency injection system that can be used to reactively reconfigure you’re application while it’s running. Reactive in this case refers to the application being reconfigured as-the-value changes, and not for other typical scenarios such as when it’s being restarted.

Values are provided as Streams of updates that can be subscribed to as necessary throughout your application.

Examples

In the following we’ll showcase the injection of a fake Database. The idea here would be that if something about the database connection changes, a new instance of Database would be created and cause the application to update.

This is available as the fake_database example and you can run it yourself using:

cargo run --example fake_database
use tokio::time;

#[derive(Clone)]
struct Database;

#[tokio::main]
async fn main() {
    let injector = async_injector::Injector::new();
    let (mut database_stream, mut database) = injector.stream::<Database>().await;

    // Insert the database dependency in a different task in the background.
    tokio::spawn({
        let injector = injector.clone();

        async move {
            time::sleep(time::Duration::from_secs(2)).await;
            injector.update(Database).await;
        }
    });

    assert!(database.is_none());
    // Every update to the stored type will be streamed, allowing you to
    // react to it.
    database = database_stream.recv().await;
    assert!(database.is_some());
}

The Injector provides a structured broadcast system of updates, that can integrate cleanly into asynchronous contexts.

With a bit of glue, this means that your application can be reconfigured without restarting it. Providing a richer user experience.

Injecting multiple things of the same type

In the previous section you might’ve noticed that the injected value was solely discriminated by its type: Database. In this example we’ll show how Key can be used to tag values of the same type under different names. This can be useful when dealing with overly generic types like String.

The tag used must be serializable with serde. It must also not use any components which cannot be hashed, like f32 and f64. This will otherwise cause an error to be raised as it’s being injected.

use async_injector::Key;
use serde::Serialize;
use std::{error::Error, time::Duration};
use tokio::time;

#[derive(Serialize)]
enum Tag {
    One,
    Two,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let injector = async_injector::Injector::new();
    let one = Key::<u32>::tagged(Tag::One)?;
    let two = Key::<u32>::tagged(Tag::Two)?;

    tokio::spawn({
        let injector = injector.clone();
        let one = one.clone();

        async move {
            let mut interval = time::interval(Duration::from_secs(1));

            for i in 0u32.. {
                interval.tick().await;
                injector.update_key(&one, i).await;
            }
        }
    });

    tokio::spawn({
        let injector = injector.clone();
        let two = two.clone();

        async move {
            let mut interval = time::interval(Duration::from_secs(1));

            for i in 0u32.. {
                interval.tick().await;
                injector.update_key(&two, i * 2).await;
            }
        }
    });

    let (mut one_stream, mut one) = injector.stream_key(one).await;
    let (mut two_stream, mut two) = injector.stream_key(two).await;

    println!("one: {:?}", one);
    println!("two: {:?}", two);

    loop {
        tokio::select! {
            update = one_stream.recv() => {
                one = update;
                println!("one: {:?}", one);
            }
            update = two_stream.recv() => {
                two = update;
                println!("two: {:?}", two);
            }
        }
    }
}

The Provider derive

The following showcases how the Provider derive can be used to conveniently wait for groups of dependencies to become available.

Below we’re waiting for two database parameters to become updated:

  • url
  • connection_limit

Also note how they’re being update asynchronously in a background thread to simulate being updated “somewhere else”. This could be an update event caused by a multitude of things, like a configuration change in a frontend.

use async_injector::{Key, Provider, Injector};
use serde::Serialize;
use std::error::Error;
use std::time::Duration;

/// Fake database connection.
#[derive(Clone, Debug, PartialEq, Eq)]
struct Database {
    url: String,
    connection_limit: u32,
}

/// Provider that describes how to construct a database.
#[derive(Serialize)]
pub enum Tag {
    DatabaseUrl,
    ConnectionLimit,
}

/// A group of database params to wait for until they become available.
#[derive(Provider)]
struct DatabaseParams {
    #[dependency(tag = "Tag::DatabaseUrl")]
    url: String,
    #[dependency(tag = "Tag::ConnectionLimit")]
    connection_limit: u32,
}

async fn update_db_params(injector: Injector, db_url: Key<String>, connection_limit: Key<u32>) {
    tokio::time::sleep(Duration::from_secs(2));

    injector.update_key(&db_url, String::from("example.com")).await;
    injector.update_key(&connection_limit, 5).await;
}

/// Fake service that runs for two seconds with a configured database.
async fn service(db: Database) {
    tokio::time::sleep(Duration::from_secs(2));
}

#[tokio::test]
async fn test_provider() -> Result<(), Box<dyn Error>> {
    let db_url = Key::<String>::tagged(Tag::DatabaseUrl)?;
    let connection_limit = Key::<u32>::tagged(Tag::ConnectionLimit)?;

    let injector = Injector::new();

    /// Set up asynchronous task that updates the parameters in the background.
    tokio::spawn(update_db_params(injector.clone(), db_url, connection_limit));

    let provider = DatabaseParams::new(&injector).await?;

    loop {
        /// Wait until database is configured.
        let database = loop {
            if let Some(update) = provider.update().await {
                break Database {
                    url: update.url,
                    connection_limit: update.connection_limit,
                };
            }
        };

        assert_eq!(
            new_database,
            Database {
                url: String::from("example.com"),
                connection_limit: 5
            }
        );

        loop {
            tokio::select! {
                _ = service(database.clone()) => {
                    break;
                }
                update = provider.update().await {
                    match update {
                        None => break,
                    }
                }
            }
        }
    }

    Ok(())
}

Structs

An injector of dependencies.

A key used to discriminate a value in the Injector.

A variable allowing for the synchronized reading of avalue in the Injector.

A stream of updates to a value in the Injector.

Enums

Errors that can be raised by various functions in the Injector.

Type Definitions

The read guard produced by Ref::read.